package org.hl7.fhir.common.hapi.validation.validator;

import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeCompositeDatatypeDefinition;
import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.ICompositeType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.fhirpath.ExpressionNode;
import org.hl7.fhir.r4.fhirpath.FHIRPathEngine;
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.r4.model.Resource;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

/**
 * This class can be used to generate resources using FHIRPath expressions.
 *
 * Note that this is an experimental feature and the API is expected to change. Ideally
 * this will be made version independent and moved out of the validation module
 * in a future release.
 *
 * @author Marcel Parciak {@code <marcel.parciak@med.uni-goettingen.de>}
 */
public class FHIRPathResourceGeneratorR4<T extends Resource> {

	private FhirContext ctx;
	private FHIRPathEngine engine;
	private Map<String, String> pathMapping;
	private T resource = null;

	private String valueToSet = null;
	private Stack<GenerationTier> nodeStack = null;

	/**
	 * The GenerationTier summarizes some variables that are needed to create FHIR
	 * elements later on.
	 */
	class GenerationTier {
		// The RuntimeDefinition of nodes
		public BaseRuntimeElementDefinition<?> nodeDefinition = null;
		// The actual nodes, i.e. the instances that hold the values
		public List<IBase> nodes = new ArrayList<>();
		// The ChildDefinition applied to the parent (i.e. one of the nodes from a lower
		// GenerationTier) to create nodes
		public BaseRuntimeChildDefinition childDefinition = null;
		// The path segment name of nodes
		public String fhirPathName = null;

		public GenerationTier() {}

		public GenerationTier(BaseRuntimeElementDefinition<?> nodeDef, IBase firstNode) {
			this.nodeDefinition = nodeDef;
			this.nodes.add(firstNode);
		}
	}

	/**
	 * Constructor without parameters, needs a call to `setMapping` later on in
	 * order to generate any Resources.
	 */
	public FHIRPathResourceGeneratorR4() {
		this.pathMapping = new HashMap<String, String>();
		this.ctx = FhirContext.forR4();
		this.engine = new FHIRPathEngine(new HapiWorkerContext(ctx, ctx.getValidationSupport()));
	}

	/**
	 * Constructor that allows to provide a mapping right away.
	 *
	 * @param mapping Map&lt;String, String&gt; a mapping of FHIRPath to value Strings
	 *                that will be used to create a Resource.
	 */
	public FHIRPathResourceGeneratorR4(Map<String, String> mapping) {
		this();
		this.setMapping(mapping);
	}

	/**
	 * Setter for the FHIRPath mapping Map instance.
	 *
	 * @param mapping Map&lt;String, String&gt; a mapping of FHIRPath to value Strings
	 *                that will be used to create a Resource.
	 */
	public void setMapping(Map<String, String> mapping) {
		this.pathMapping = mapping;
	}

	/**
	 * Getter for a generated Resource. null if no Resource has been generated yet.
	 *
	 * @return T the generated Resource or null.
	 */
	public T getResource() {
		return this.resource;
	}

	/**
	 * Prepares the internal state prior to generating a FHIR Resource. Called once
	 * upon generation at the start.
	 *
	 * @param resourceClass Class<T> The class of the Resource that shall be created
	 *                      (an empty Resource will be created in this method).
	 */
	@SuppressWarnings("unchecked")
	private void prepareInternalState(Class<T> resourceClass) {
		this.resource = (T) this.ctx.getResourceDefinition(resourceClass).newInstance();
	}

	/**
	 * The generation method that yields a new instance of class `resourceClass`
	 * with every value set in the FHIRPath mapping.
	 *
	 * @param resourceClass Class<T> The class of the Resource that shall be
	 *                      created.
	 * @return T a new FHIR Resource instance of class `resourceClass`.
	 */
	public T generateResource(Class<T> resourceClass) {
		this.prepareInternalState(resourceClass);

		for (String fhirPath : this.sortedPaths()) {
			// prepare the next fhirPath iteration: create a new nodeStack and set the value
			this.nodeStack = new Stack<>();
			this.nodeStack.push(new GenerationTier(this.ctx.getResourceDefinition(this.resource), this.resource));
			this.valueToSet = this.pathMapping.get(fhirPath);

			// pathNode is the part of the FHIRPath we are processing
			ExpressionNode pathNode = this.engine.parse(fhirPath);
			while (pathNode != null) {
				switch (pathNode.getKind()) {
					case Name:
						this.handleNameNode(pathNode);
						break;
					case Function:
						this.handleFunctionNode(pathNode);
						break;
					case Constant:
					case Group:
					case Unary:
						// TODO: unimplmemented, what to do?
						break;
				}
				pathNode = pathNode.getInner();
			}
		}

		this.nodeStack = null;
		return this.resource;
	}

	/*
	 * Handling Named nodes
	 */

	/**
	 * Handles a named node, either adding a new layer to the `nodeStack` when
	 * reaching a Composite Node or adding the value for Primitive Nodes.
	 *
	 * @param fhirPath String the FHIRPath section for the next GenerationTier.
	 */
	private void handleNameNode(ExpressionNode fhirPath) {
		BaseRuntimeChildDefinition childDef =
				this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName());
		if (childDef == null) {
			// nothing to do
			return;
		}

		// identify the type of named node we need to handle here by getting the runtime
		// definition type
		switch (childDef.getChildByName(fhirPath.getName()).getChildType()) {
			case COMPOSITE_DATATYPE:
				handleCompositeNode(fhirPath);
				break;

			case PRIMITIVE_DATATYPE:
				handlePrimitiveNode(fhirPath);
				break;

			case ID_DATATYPE:
			case RESOURCE:
			case CONTAINED_RESOURCE_LIST:
			case CONTAINED_RESOURCES:
			case EXTENSION_DECLARED:
			case PRIMITIVE_XHTML:
			case PRIMITIVE_XHTML_HL7ORG:
			case RESOURCE_BLOCK:
			case UNDECL_EXT:
				// TODO: not implemented. What to do?
		}
	}

	/**
	 * Handles primitive nodes with regards to the current latest tier of the
	 * nodeStack. Sets a primitive value to all nodes.
	 *
	 * @param fhirPath ExpressionNode segment of the fhirPath that specifies the
	 *                 primitive value to set.
	 */
	private void handlePrimitiveNode(ExpressionNode fhirPath) {
		// Get the child definition from the parent
		BaseRuntimeChildDefinition childDefinition =
				this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName());
		// Get the primitive type definition from the childDeftinion
		RuntimePrimitiveDatatypeDefinition primitiveTarget =
				(RuntimePrimitiveDatatypeDefinition) childDefinition.getChildByName(fhirPath.getName());
		for (IBase nodeElement : this.nodeStack.peek().nodes) {
			// add the primitive value to each parent node
			IPrimitiveType<?> primitive =
					primitiveTarget.newInstance(childDefinition.getInstanceConstructorArguments());
			primitive.setValueAsString(this.valueToSet);
			childDefinition.getMutator().addValue(nodeElement, primitive);
		}
	}

	/**
	 * Handles a composite node with regards to the current latest tier of the
	 * nodeStack. Creates a new node based on fhirPath if none are available.
	 *
	 * @param fhirPath ExpressionNode the segment of the FHIRPath that is being
	 *                 handled right now.
	 */
	private void handleCompositeNode(ExpressionNode fhirPath) {
		GenerationTier nextTier = new GenerationTier();
		// get the name of the FHIRPath for the next tier
		nextTier.fhirPathName = fhirPath.getName();
		// get the child definition from the parent nodePefinition
		nextTier.childDefinition = this.nodeStack.peek().nodeDefinition.getChildByName(fhirPath.getName());
		// create a nodeDefinition for the next tier
		nextTier.nodeDefinition = nextTier.childDefinition.getChildByName(nextTier.fhirPathName);

		RuntimeCompositeDatatypeDefinition compositeTarget =
				(RuntimeCompositeDatatypeDefinition) nextTier.nodeDefinition;
		// iterate through all parent nodes
		for (IBase nodeElement : this.nodeStack.peek().nodes) {
			List<IBase> containedNodes = nextTier.childDefinition.getAccessor().getValues(nodeElement);
			if (containedNodes.size() > 0) {
				// check if sister nodes are already available
				nextTier.nodes.addAll(containedNodes);
			} else {
				// if not nodes are available, create a new node
				ICompositeType compositeNode =
						compositeTarget.newInstance(nextTier.childDefinition.getInstanceConstructorArguments());
				nextTier.childDefinition.getMutator().addValue(nodeElement, compositeNode);
				nextTier.nodes.add(compositeNode);
			}
		}
		// push the created nextTier to the nodeStack
		this.nodeStack.push(nextTier);
	}

	/*
	 * Handling Function Nodes
	 */

	/**
	 * Handles a function node of a FHIRPath.
	 *
	 * @param fhirPath ExpressionNode the segment of the FHIRPath that is being
	 *                 handled right now.
	 */
	private void handleFunctionNode(ExpressionNode fhirPath) {
		switch (fhirPath.getFunction()) {
			case Where:
				this.handleWhereFunctionNode(fhirPath);
				break;
			case MatchesFull:
			case Aggregate:
			case All:
			case AllFalse:
			case AllTrue:
			case AnyFalse:
			case AnyTrue:
			case As:
			case Check:
			case Children:
			case Combine:
			case ConformsTo:
			case Contains:
			case ConvertsToBoolean:
			case ConvertsToDateTime:
			case ConvertsToDecimal:
			case ConvertsToInteger:
			case ConvertsToQuantity:
			case ConvertsToString:
			case ConvertsToTime:
			case Count:
			case Custom:
			case DefineVariable:
			case Descendants:
			case Distinct:
			case Empty:
			case EndsWith:
			case Exclude:
			case Exists:
			case Extension:
			case First:
			case HasValue:
			case Iif:
			case IndexOf:
			case Intersect:
			case Is:
			case IsDistinct:
			case Item:
			case Last:
			case Length:
			case Lower:
			case Matches:
			case MemberOf:
			case Not:
			case Now:
			case OfType:
			case Repeat:
			case Replace:
			case ReplaceMatches:
			case Resolve:
			case Select:
			case Single:
			case Skip:
			case StartsWith:
			case SubsetOf:
			case Substring:
			case SupersetOf:
			case Tail:
			case Take:
			case ToBoolean:
			case ToChars:
			case ToDateTime:
			case ToDecimal:
			case ToInteger:
			case ToQuantity:
			case ToString:
			case ToTime:
			case Today:
			case Trace:
			case Type:
			case Union:
			case Upper:
				// TODO: unimplemented, what to do?
			case ConvertsToDate:
				break;
			case Round:
				break;
			case Sqrt:
				break;
			case Abs:
				break;
			case Ceiling:
				break;
			case Exp:
				break;
			case Floor:
				break;
			case Ln:
				break;
			case Log:
				break;
			case Power:
				break;
			case Truncate:
				break;
			case Encode:
				break;
			case Decode:
				break;
			case Escape:
				break;
			case Unescape:
				break;
			case Trim:
				break;
			case Split:
				break;
			case Join:
				break;
			case LowBoundary:
				break;
			case HighBoundary:
				break;
			case Precision:
				break;
			case HtmlChecks1:
				break;
			case HtmlChecks2:
				break;
		}
	}

	/**
	 * Handles a function node of a `where`-function. Iterates through all params
	 * and handle where functions for primitive datatypes (others are not
	 * implemented and yield errors.)
	 *
	 * @param fhirPath ExpressionNode the segment of the FHIRPath that contains the
	 *                 where function
	 */
	private void handleWhereFunctionNode(ExpressionNode fhirPath) {
		// iterate through all where parameters
		for (ExpressionNode param : fhirPath.getParameters()) {
			BaseRuntimeChildDefinition wherePropertyChild =
					this.nodeStack.peek().nodeDefinition.getChildByName(param.getName());
			BaseRuntimeElementDefinition<?> wherePropertyDefinition =
					wherePropertyChild.getChildByName(param.getName());

			// only primitive nodes can be checked using the where function
			switch (wherePropertyDefinition.getChildType()) {
				case PRIMITIVE_DATATYPE:
					this.handleWhereFunctionParam(param);
					break;
				case COMPOSITE_DATATYPE:
				case CONTAINED_RESOURCES:
				case CONTAINED_RESOURCE_LIST:
				case EXTENSION_DECLARED:
				case ID_DATATYPE:
				case PRIMITIVE_XHTML:
				case PRIMITIVE_XHTML_HL7ORG:
				case RESOURCE:
				case RESOURCE_BLOCK:
				case UNDECL_EXT:
					// TODO: unimplemented. What to do?
			}
		}
	}

	/**
	 * Filter the latest nodeStack tier using `param`.
	 *
	 * @param param ExpressionNode parameter type ExpressionNode that provides the
	 *              where clause that is used to filter nodes from the nodeStack.
	 */
	private void handleWhereFunctionParam(ExpressionNode param) {
		BaseRuntimeChildDefinition wherePropertyChild =
				this.nodeStack.peek().nodeDefinition.getChildByName(param.getName());
		BaseRuntimeElementDefinition<?> wherePropertyDefinition = wherePropertyChild.getChildByName(param.getName());

		String matchingValue = param.getOpNext().getConstant().toString();
		List<IBase> matchingNodes = new ArrayList<>();
		List<IBase> unlabeledNodes = new ArrayList<>();
		// sort all nodes from the nodeStack into matching nodes and unlabeled nodes
		for (IBase node : this.nodeStack.peek().nodes) {
			List<IBase> operationValues = wherePropertyChild.getAccessor().getValues(node);
			if (operationValues.size() == 0) {
				unlabeledNodes.add(node);
			} else {
				for (IBase operationValue : operationValues) {
					IPrimitiveType<?> primitive = (IPrimitiveType<?>) operationValue;
					switch (param.getOperation()) {
						case Equals:
							if (primitive.getValueAsString().equals(matchingValue)) {
								matchingNodes.add(node);
							}
							break;
						case NotEquals:
							if (!primitive.getValueAsString().equals(matchingValue)) {
								matchingNodes.add(node);
							}
							break;
						case And:
						case As:
						case Concatenate:
						case Contains:
						case Div:
						case DivideBy:
						case Equivalent:
						case Greater:
						case GreaterOrEqual:
						case Implies:
						case In:
						case Is:
						case LessOrEqual:
						case LessThan:
						case MemberOf:
						case Minus:
						case Mod:
						case NotEquivalent:
						case Or:
						case Plus:
						case Times:
						case Union:
						case Xor:
							// TODO: unimplemented, what to do?
					}
				}
			}
		}

		if (matchingNodes.size() == 0) {
			if (unlabeledNodes.size() == 0) {
				// no nodes were matched and no unlabeled nodes are available. We need to add a
				// sister node to the nodeStack
				GenerationTier latestTier = this.nodeStack.pop();
				GenerationTier previousTier = this.nodeStack.peek();
				this.nodeStack.push(latestTier);

				RuntimeCompositeDatatypeDefinition compositeTarget =
						(RuntimeCompositeDatatypeDefinition) latestTier.nodeDefinition;
				ICompositeType compositeNode =
						compositeTarget.newInstance(latestTier.childDefinition.getInstanceConstructorArguments());
				latestTier.childDefinition.getMutator().addValue(previousTier.nodes.get(0), compositeNode);
				unlabeledNodes.add(compositeNode);
			}

			switch (param.getOperation()) {
				case Equals:
					// if we are checking for equality, we need to set the property we looked for on
					// the unlabeled node(s)
					RuntimePrimitiveDatatypeDefinition equalsPrimitive =
							(RuntimePrimitiveDatatypeDefinition) wherePropertyDefinition;
					IPrimitiveType<?> primitive =
							equalsPrimitive.newInstance(wherePropertyChild.getInstanceConstructorArguments());
					primitive.setValueAsString(param.getOpNext().getConstant().toString());
					for (IBase node : unlabeledNodes) {
						wherePropertyChild.getMutator().addValue(node, primitive);
						matchingNodes.add(node);
					}
					break;
				case NotEquals:
					// if we are checking for inequality, we need to pass all unlabeled (or created
					// if none were available)
					matchingNodes.addAll(unlabeledNodes);
					break;
				case And:
				case As:
				case Concatenate:
				case Contains:
				case Div:
				case DivideBy:
				case Equivalent:
				case Greater:
				case GreaterOrEqual:
				case Implies:
				case In:
				case Is:
				case LessOrEqual:
				case LessThan:
				case MemberOf:
				case Minus:
				case Mod:
				case NotEquivalent:
				case Or:
				case Plus:
				case Times:
				case Union:
				case Xor:
					// TODO: need to implement above first
			}
		}

		// set the nodes to the filtered ones
		this.nodeStack.peek().nodes = matchingNodes;
	}

	/**
	 * Creates a list all FHIRPaths from the mapping ordered by paths with where
	 * equals, where unequals and the rest.
	 *
	 * @return List<String> a List of FHIRPaths ordered by the type.
	 */
	private List<String> sortedPaths() {
		List<String> whereEquals = new ArrayList<String>();
		List<String> whereUnequals = new ArrayList<String>();
		List<String> withoutWhere = new ArrayList<String>();

		for (String fhirPath : this.pathMapping.keySet()) {
			switch (this.getTypeOfFhirPath(fhirPath)) {
				case WHERE_EQUALS:
					whereEquals.add(fhirPath);
					break;
				case WHERE_UNEQUALS:
					whereUnequals.add(fhirPath);
					break;
				case WITHOUT_WHERE:
					withoutWhere.add(fhirPath);
					break;
			}
		}

		List<String> ret = new ArrayList<String>();
		ret.addAll(whereEquals);
		ret.addAll(whereUnequals);
		ret.addAll(withoutWhere);
		return ret;
	}

	/**
	 * Returns the type of path based on the FHIRPath String.
	 *
	 * @param fhirPath String representation of a FHIRPath.
	 * @return PathType the type of path supplied as `fhirPath`.
	 */
	private PathType getTypeOfFhirPath(String fhirPath) {
		ExpressionNode fhirPathExpression = this.engine.parse(fhirPath);
		while (fhirPathExpression != null) {
			if (fhirPathExpression.getKind() == ExpressionNode.Kind.Function) {
				if (fhirPathExpression.getFunction() == ExpressionNode.Function.Where) {
					for (ExpressionNode params : fhirPathExpression.getParameters()) {
						switch (params.getOperation()) {
							case Equals:
								return PathType.WHERE_EQUALS;
							case NotEquals:
								return PathType.WHERE_UNEQUALS;
							case And:
							case As:
							case Concatenate:
							case Contains:
							case Div:
							case DivideBy:
							case Equivalent:
							case Greater:
							case GreaterOrEqual:
							case Implies:
							case In:
							case Is:
							case LessOrEqual:
							case LessThan:
							case MemberOf:
							case Minus:
							case Mod:
							case NotEquivalent:
							case Or:
							case Plus:
							case Times:
							case Union:
							case Xor:
								// TODO: need to implement above first
						}
					}
				}
			}
			fhirPathExpression = fhirPathExpression.getInner();
		}
		return PathType.WITHOUT_WHERE;
	}

	/**
	 * A simple enum to diffirentiate between types of FHIRPaths in the special use
	 * case of generating FHIR Resources.
	 */
	public enum PathType {
		WHERE_EQUALS,
		WHERE_UNEQUALS,
		WITHOUT_WHERE
	}
}
